// Copyright (C) 2016 Kurt Pattyn <pattyn.kurt@gmail.com>.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

/*!
    \qmltype WebSocket
    \nativetype QQmlWebSocket
    \since 5.3

    \inqmlmodule QtWebSockets
    \ingroup websockets-qml
    \brief QML interface to QWebSocket.

    WebSockets is a web technology providing full-duplex communications channels over a
    single TCP connection.
    The WebSocket protocol was standardized by the IETF as \l {RFC 6455} in 2011.
*/

/*!
  \qmlproperty url WebSocket::url
  Server url to connect to. The url must have one of 2 schemes: \e ws:// or \e wss://.
  When not supplied, then \e ws:// is used.
*/

/*!
  \qmlproperty list<string> WebSocket::requestedSubprotocols
  \since 6.4
  The list of WebSocket subprotocols to send in the WebSocket handshake.
*/

/*!
  \qmlproperty string WebSocket::negotiatedSubprotocol
  \since 6.4
  The WebSocket subprotocol that has been negotiated with the server.
*/

/*!
  \qmlproperty Status WebSocket::status
  Status of the WebSocket.

  The status can have the following values:
  \list
  \li WebSocket.Connecting
  \li WebSocket.Open
  \li WebSocket.Closing
  \li WebSocket.Closed
  \li WebSocket.Error
  \endlist
*/

/*!
  \qmlproperty string WebSocket::errorString
  Contains a description of the last error that occurred. When no error occurrred,
  this string is empty.
*/

/*!
  \qmlproperty bool WebSocket::active
  When set to true, a connection is made to the server with the given url.
  When set to false, the connection is closed.
  The default value is false.
*/

/*!
  \qmlsignal WebSocket::textMessageReceived(QString message)
  This signal is emitted when a text message is received.
  \a message contains the bytes received.
*/

/*!
  \qmlsignal WebSocket::binaryMessageReceived(QString message)
  \since 5.8
  This signal is emitted when a binary message is received.
  \a message contains the bytes received.
*/

/*!
  \qmlsignal WebSocket::statusChanged(Status status)
  This signal is emitted when the status of the WebSocket changes.
  The \a status argument provides the current status.

  \sa {QtWebSockets::}{WebSocket::status}
*/

/*!
  \qmlmethod void WebSocket::sendTextMessage(string message)
  Sends \a message to the server.
*/

/*!
  \qmlmethod void WebSocket::sendBinaryMessage(ArrayBuffer message)
  \since 5.8
  Sends the parameter \a message to the server.
*/

/*!
  \qmlmethod void WebSocket::ping(ArrayBuffer payload)
  \since 6.10
  Pings the server to indicate that the connection is still alive.
  An additional \a payload can be sent along with the ping message.

  The size of the \a payload cannot be bigger than 125 bytes.
  If it is larger, the \a payload is clipped to 125 bytes.

  \note QWebSocket and QWebSocketServer handles ping requests internally,
  which means they automatically send back a pong response to the peer.

  \sa pong()
*/

/*!
  \qmlsignal WebSocket::pong(quint64 elapsedTime, const QByteArray &payload)
  \since 6.10
  Emitted when a pong message is received in reply to a previous ping.
  \a elapsedTime contains the roundtrip time in milliseconds and \a payload contains an optional
  payload that was sent with the ping.

  \sa ping()
*/

#include "qqmlwebsocket.h"
#include <QtWebSockets/QWebSocket>
#include <QtWebSockets/QWebSocketHandshakeOptions>

QT_BEGIN_NAMESPACE

QQmlWebSocket::QQmlWebSocket(QObject *parent) :
    QObject(parent),
    m_webSocket(),
    m_status(Closed),
    m_url(),
    m_isActive(false),
    m_componentCompleted(true),
    m_errorString()
{
}

QQmlWebSocket::QQmlWebSocket(QWebSocket *socket, QObject *parent) :
    QObject(parent),
    m_status(Closed),
    m_url(socket->requestUrl()),
    m_requestedProtocols(socket->handshakeOptions().subprotocols()),
    m_isActive(true),
    m_componentCompleted(true),
    m_errorString(socket->errorString())
{
    setSocket(socket);
    onStateChanged(socket->state());
}

QQmlWebSocket::~QQmlWebSocket()
{
    if (m_webSocket)
        m_webSocket->disconnect();
}

qint64 QQmlWebSocket::sendTextMessage(const QString &message)
{
    if (m_status != Open) {
        setErrorString(tr("Messages can only be sent when the socket is open."));
        setStatus(Error);
        return 0;
    }
    return m_webSocket->sendTextMessage(message);
}

qint64 QQmlWebSocket::sendBinaryMessage(const QByteArray &message)
{
    if (m_status != Open) {
        setErrorString(tr("Messages can only be sent when the socket is open."));
        setStatus(Error);
        return 0;
    }
    return m_webSocket->sendBinaryMessage(message);
}

void QQmlWebSocket::ping(const QByteArray &payload)
{
    if (m_status != Open) {
        setErrorString(tr("Messages can only be sent when the socket is open."));
        setStatus(Error);
    }
    m_webSocket->ping(payload);
}

QStringList QQmlWebSocket::requestedSubprotocols() const
{
    return m_requestedProtocols;
}

void QQmlWebSocket::setRequestedSubprotocols(const QStringList &requestedSubprotocols)
{
    if (m_requestedProtocols == requestedSubprotocols)
        return;

    m_requestedProtocols = requestedSubprotocols;
    emit requestedSubprotocolsChanged();
}

QUrl QQmlWebSocket::url() const
{
    return m_url;
}

void QQmlWebSocket::setUrl(const QUrl &url)
{
    if (m_url == url) {
        return;
    }
    if (m_webSocket && (m_status == Open)) {
        m_webSocket->close();
    }
    m_url = url;
    Q_EMIT urlChanged();
    open();
}

QString QQmlWebSocket::negotiatedSubprotocol() const
{
    return m_negotiatedProtocol;
}

QQmlWebSocket::Status QQmlWebSocket::status() const
{
    return m_status;
}

QString QQmlWebSocket::errorString() const
{
    return m_errorString;
}

void QQmlWebSocket::classBegin()
{
    m_componentCompleted = false;
    m_errorString = tr("QQmlWebSocket is not ready.");
    m_status = Closed;
}

void QQmlWebSocket::componentComplete()
{
    setSocket(new QWebSocket);

    m_componentCompleted = true;

    open();
}

void QQmlWebSocket::setSocket(QWebSocket *socket)
{
    m_webSocket.reset(socket);
    if (m_webSocket) {
        // explicit ownership via QScopedPointer
        m_webSocket->setParent(nullptr);
        connect(m_webSocket.data(), &QWebSocket::textMessageReceived,
                this, &QQmlWebSocket::textMessageReceived);
        connect(m_webSocket.data(), &QWebSocket::binaryMessageReceived,
                this, &QQmlWebSocket::binaryMessageReceived);
        connect(m_webSocket.data(), &QWebSocket::pong,
                this, &QQmlWebSocket::pong);
        connect(m_webSocket.data(), &QWebSocket::errorOccurred,
                this, &QQmlWebSocket::onError);
        connect(m_webSocket.data(), &QWebSocket::stateChanged,
                this, &QQmlWebSocket::onStateChanged);
    }
}

void QQmlWebSocket::onError(QAbstractSocket::SocketError error)
{
    Q_UNUSED(error);
    setErrorString(m_webSocket->errorString());
    setStatus(Error);
}

void QQmlWebSocket::onStateChanged(QAbstractSocket::SocketState state)
{
    switch (state)
    {
        case QAbstractSocket::ConnectingState:
        case QAbstractSocket::BoundState:
        case QAbstractSocket::HostLookupState:
        {
            setStatus(Connecting);
            break;
        }
        case QAbstractSocket::UnconnectedState:
        {
            setStatus(Closed);
            break;
        }
        case QAbstractSocket::ConnectedState:
        {
            setStatus(Open);
            break;
        }
        case QAbstractSocket::ClosingState:
        {
            setStatus(Closing);
            break;
        }
        default:
        {
            setStatus(Connecting);
            break;
        }
    }
}

void QQmlWebSocket::setStatus(QQmlWebSocket::Status status)
{
    if (m_status == status) {
        return;
    }
    m_status = status;
    if (status != Error) {
        setErrorString();
    }
    Q_EMIT statusChanged(m_status);

    const auto protocol = m_status == Open && m_webSocket ? m_webSocket->subprotocol() : QString();
    if (m_negotiatedProtocol != protocol) {
        m_negotiatedProtocol = protocol;
        Q_EMIT negotiatedSubprotocolChanged();
    }
}

void QQmlWebSocket::setActive(bool active)
{
    if (m_isActive == active) {
        return;
    }
    m_isActive = active;
    Q_EMIT activeChanged(m_isActive);
    if (!m_componentCompleted) {
        return;
    }
    if (m_isActive) {
        open();
    } else {
        close();
    }
}

bool QQmlWebSocket::isActive() const
{
    return m_isActive;
}

void QQmlWebSocket::open()
{
    if (m_componentCompleted && m_isActive && m_url.isValid() && Q_LIKELY(m_webSocket)) {
        QWebSocketHandshakeOptions opt;
        opt.setSubprotocols(m_requestedProtocols);
        m_webSocket->open(m_url, opt);
    }
}

void QQmlWebSocket::close()
{
    if (m_componentCompleted && Q_LIKELY(m_webSocket)) {
        m_webSocket->close();
    }
}

void QQmlWebSocket::setErrorString(QString errorString)
{
    if (m_errorString == errorString) {
        return;
    }
    m_errorString = errorString;
    Q_EMIT errorStringChanged(m_errorString);
}

QT_END_NAMESPACE
